using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using Microsoft.Cryptography.Diagnostics;

namespace Microsoft.Cryptography
{
    /// <summary>
    /// The default RSACryptoServiceProvider class can't handle RSA-signatures, which
    /// are using SHA-2 or RIPEMD-160 as hash-algorithms. Even if the method
    /// VerifyHash is used and the correct calculated hash is given as parameter an
    /// error occurs.
    /// To circumvent these problems use this class, which implements the RSA algorithm
    /// managed and is using RSACryptoServiceProvider only if possible (for encryption,
    /// decryption and signature with SHA-1 and MD5)
    /// </summary>
    public sealed class RSAManaged : RSABase
    {
        #region constructors

        /// <summary>
        /// RSA Managed standard constructor.
        /// </summary>
        public RSAManaged()
        {
        }

        /// <summary>
        /// RSAManged Constructor.
        /// </summary>
        /// <param name="rsaParameters">RSAParameter to import.</param>
        public RSAManaged(RSAParameters rsaParameters)
        {
            ImportParameters(rsaParameters);
        }

        #endregion

        #region private fields

        /// <summary>
        /// Current imported RSA-Parameters.
        /// </summary>
        private Nullable<RSAParameters> _rsaParams;
        private RSACryptoServiceProvider _rsaCSP = null;

        #endregion

        /// <summary>
        /// RSA-Parameters, which should be used
        /// </summary>
        public Nullable<RSAParameters> RSAParameters
        {
            get { return _rsaParams; }
        }

        private RSACryptoServiceProvider CryptoServiceProvider
        {
            get
            {
                if (_rsaCSP == null)
                {
                    lock (this)
                    {
                        if (_rsaCSP == null)
                        {
                            if (_rsaParams == null)
                            {
                                throw new ArgumentNullException("RSAParameters");
                            }

                            _rsaCSP = new RSACryptoServiceProvider();
                            _rsaCSP.ImportParameters(_rsaParams.Value);
                        }
                    }
                }

                return _rsaCSP;
            }
        }

        /// <summary>
        /// Decrypt the given byte-array by using the currently imported RSA-Parameters.
        /// </summary>
        /// <param name="rgb">Data to decrypt as byte array.</param>
        /// <returns></returns>
        public override byte[] DecryptValue(byte[] rgb)
        {
            throw new NotSupportedException();

            //// Check the data against null.
            //if (rgb == null)
            //{
            //    throw new ArgumentNullException("rgb");
            //}
            //if (_rsaParams == null)
            //{
            //    throw new Exception(ErrorText.RSAParamsNotSet);
            //}
            //// Make sure that we have sufficient RSA parameters.
            //if (_rsaParams.Value.Modulus == null)
            //{
            //    throw new Exception(ErrorText.RSAParamsNotSet);
            //}

            //return CryptoServiceProvider.Decrypt(rgb, false);
        }

        /// <summary>
        /// Encrypt the given byte-array by using the currently imported RSA-Parameters.
        /// </summary>
        /// <param name="rgb">Data to encrypt as byte array.</param>
        /// <returns></returns>
        public override byte[] EncryptValue(byte[] rgb)
        {
            throw new NotSupportedException();

            //// Check the data against null.
            //if (rgb == null)
            //{
            //    throw new ArgumentNullException("rgb");
            //}
            //if (_rsaParams == null)
            //{
            //    throw new Exception(ErrorText.RSAParamsNotSet);
            //}
            //// Make sure that we have sufficient RSA parameters.
            //if (_rsaParams.Value.Modulus == null)
            //{
            //    throw new Exception(ErrorText.RSAParamsNotSet);
            //}

            //return CryptoServiceProvider.Encrypt(rgb, false);
        }

        /// <summary>
        /// Overridden. Exports the RSAParameters.
        /// </summary>
        /// <param name="includePrivateParameters">true to include private parameters; otherwise, false. </param>
        /// <returns>The parameters for RSA.</returns>
        public override RSAParameters ExportParameters(bool includePrivateParameters)
        {
            if (_rsaParams == null)
            {
                throw new ArgumentNullException("_rsaParameters");
            }
            if (_rsaParams.Value.Modulus == null)
            {
                throw new Exception(ErrorText.RSAParamsNotSet);
            }

            if (includePrivateParameters == true)
            {
                return _rsaParams.Value;
            }
            else
            {
                RSAParameters paramWithoutPrivate = new RSAParameters();
                paramWithoutPrivate.Modulus = _rsaParams.Value.Modulus;
                paramWithoutPrivate.Exponent = _rsaParams.Value.Exponent;
                paramWithoutPrivate.D = null;
                paramWithoutPrivate.DP = null;
                paramWithoutPrivate.DQ = null;
                paramWithoutPrivate.InverseQ = null;
                paramWithoutPrivate.P = null;
                paramWithoutPrivate.Q = null;
                return paramWithoutPrivate;
            }
        }

        /// <summary>
        /// Overridden. Imports the specified RSAParameters.
        /// </summary>
        /// <param name="parameters">The parameters for RSA.</param>
        public override void ImportParameters(RSAParameters parameters)
        {
            if (parameters.Modulus == null || parameters.Exponent == null)
            {
                throw new Exception(ErrorText.InvalidRSAParams);
            }

            _rsaParams = parameters;
            _rsaCSP = null;
        }

        /// <summary>
        /// Dispose method.
        /// </summary>
        /// <param name="disposing"></param>
        protected override void Dispose(bool disposing)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        public override string KeyExchangeAlgorithm
        {
            get { return CryptoServiceProvider.KeyExchangeAlgorithm; }
        }

        /// <summary>
        /// 
        /// </summary>
        public override string SignatureAlgorithm
        {
            get 
            { 
                return CryptoServiceProvider.SignatureAlgorithm; 
            }
        }

        /// <summary>
        /// Compute a hash value over a buffer and sign it.
        /// </summary>
        /// <param name="buffer">Buffer to sign.</param>
        /// <param name="halg">Hashalgorithm for signing.</param>
        /// <returns>Signing succeed.</returns>
        public override byte[] SignData(byte[] buffer, object halg)
        {
            string hashName;
            HashAlgorithm alg = ObjectToHashAlg(halg, out hashName);
            byte[] rgbHash = alg.ComputeHash(buffer);
            return SignHash(rgbHash, hashName);
        }

        /// <summary>
        /// Sign an explicit hash value.
        /// </summary>
        /// <param name="rgbHash"></param>
        /// <param name="halg"></param>
        /// <returns>Signing succeed.</returns>
        public override byte[] SignHash(byte[] rgbHash, string halg)
        {
            /*
8.2.1 Signature generation operation

   RSASSA-PKCS1-V1_5-SIGN (K, M)

   Input:
   K        signer's RSA private key
   M        message to be signed, an octet string

   Output:
   S        signature, an octet string of length k, where k is the
            length in octets of the RSA modulus n

   Errors: "message too long"; "RSA modulus too short"

   Steps:

   1. EMSA-PKCS1-v1_5 encoding: Apply the EMSA-PKCS1-v1_5 encoding
      operation (Section 9.2) to the message M to produce an encoded
      message EM of length k octets:

         EM = EMSA-PKCS1-V1_5-ENCODE (M, k).

      If the encoding operation outputs "message too long," output
      "message too long" and stop.  If the encoding operation outputs
      "intended encoded message length too short," output "RSA modulus
      too short" and stop.




Jonsson & Kaliski            Informational                     [Page 33]
--------------------------------------------------------------------------------

RFC 3447        PKCS #1: RSA Cryptography Specifications   February 2003


   2. RSA signature:

      a. Convert the encoded message EM to an integer message
         representative m (see Section 4.2):

            m = OS2IP (EM).

      b. Apply the RSASP1 signature primitive (Section 5.2.1) to the RSA
         private key K and the message representative m to produce an
         integer signature representative s:

            s = RSASP1 (K, m).

      c. Convert the signature representative s to a signature S of
         length k octets (see Section 4.1):

            S = I2OSP (s, k).

   3. Output the signature S.

*/

            // Validate the parameters.
            if (rgbHash == null)
            {
                throw new ArgumentNullException("rgbHash");
            }
            if (halg == null)
            {
                throw new ArgumentNullException("halg");
            }

            if (_rsaParams == null)
            {
                throw new Exception(ErrorText.RSAParamsNotSet);
            }

            byte[] header = HashAlgToHeader(halg);
            if (rgbHash.Length != header[header.Length - 1])
            {
                throw new Exception(ErrorText.InvalidHashSize);
            }

            // Make sure that we have sufficient RSA parameters.
            if (_rsaParams.Value.Modulus == null)
            {
                throw new Exception(ErrorText.RSAParamsNotSet);
            }

            // Check the length of the incoming value.
            int k = _rsaParams.Value.Modulus.Length;
            if (k < (header.Length + rgbHash.Length + 11))
            {
                throw new Exception(ErrorText.RSAKeyTooShort);
            }

            string hashName = "";
            HashAlgorithm alg = ObjectToHashAlg(halg, out hashName);
            
            // Format the value to be signed.
            byte[] msg = RsaPkcs1Padding(k, header, rgbHash);

            // Sign the value with the private key.
            RsaCalculator rsaCalc = new RsaCalculator((RSAParameters)_rsaParams);

            string dataBinHex = ByteTools.ToBinHex(msg, "X");
            
            System.Diagnostics.Stopwatch watchDoCrypt = new System.Diagnostics.Stopwatch();
            watchDoCrypt.Start();
            
            rsaCalc.DoCrypt(RsaCalculator.KeyType.RsaPrivate, RsaCalculator.RsaDir.Encrypt, dataBinHex);
            byte[] signedValue = rsaCalc.BytesOutput;

            watchDoCrypt.Stop();
            LogProvider.LogMessage(LogProvider.LogPriorities.Info, LogProvider.LogSources.CRYPTO_Basis, "SignHash.DoCrypt: {0} ms", watchDoCrypt.ElapsedMilliseconds);

            if (signedValue.Length < k)
            {
                // Zero-extend the value if necessary.
                byte[] zextend = new byte[k];
                Array.Copy(signedValue, 0, zextend, k - signedValue.Length,
                           signedValue.Length);
                Array.Clear(signedValue, 0, signedValue.Length);
                signedValue = zextend;
            }

            // Destroy sensitive values.
            Array.Clear(msg, 0, msg.Length);


            // Done.
            return signedValue;
        }

        // Convert a hash algorithm name into a PKCS #1 header.
        private byte[] HashAlgToHeader(String alg)
        {
            
            if (alg == "MD5" || alg == "System.Security.Cryptography.MD5CryptoServiceProvider")
            {
                return md5Header;
            }
            else if (alg == null || alg == "SHA1" || alg == "System.Security.Cryptography.SHA1CryptoServiceProvider" || alg == "System.Security.Cryptography.SHA1Managed")
            {
                return sha1Header;
            }
            else if (alg == "SHA256" || alg == "System.Security.Cryptography.SHA256Managed" || alg == "System.Security.Cryptography.SHA256CryptoServiceProvider")
            {
                return sha256Header;
            }
            else if (alg == "SHA384" || alg == "System.Security.Cryptography.SHA384Managed" || alg == "System.Security.Cryptography.SHA384CryptoServiceProvider")
            {
                return sha384Header;
            }
            else if (alg == "SHA512" || alg == "System.Security.Cryptography.SHA512Managed" || alg == "System.Security.Cryptography.SHA512CryptoServiceProvider")
            {
                return sha512Header;
            }
            else if (alg == "RIPEMD160" || alg == "System.Security.Cryptography.RIPEMD160Managed")
            {
                return ripeMd160Header;
            }
            else
            {
                throw new Exception(ErrorText.HashAlgNotSupported);
            }
        }
        
        /// <summary>
        /// Verify the signature on a buffer of data.
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="halg"></param>
        /// <param name="signatureData"></param>
        /// <returns></returns>
        public override bool VerifyData(byte[] buffer, object halg, byte[] signatureData)
        {
            try
            {
                System.Diagnostics.Stopwatch watchVerifyData = new System.Diagnostics.Stopwatch();
                watchVerifyData.Start();
                string hashName = "";
                HashAlgorithm alg = ObjectToHashAlg(halg, out hashName);

                System.Diagnostics.Stopwatch watchGetHash = new System.Diagnostics.Stopwatch();
                watchGetHash.Start();
                byte[] rgbHash = alg.ComputeHash( buffer);
                watchGetHash.Stop();

                System.Diagnostics.Stopwatch watchVerifyHash = new System.Diagnostics.Stopwatch();
                watchVerifyHash.Start();
                bool returnValue = VerifyHash(rgbHash, halg, signatureData);
                watchVerifyHash.Stop();
                watchVerifyData.Stop();

                LogProvider.LogMessage(LogProvider.LogPriorities.Info, LogProvider.LogSources.CRYPTO_Basis, "VerifyData: {0} ms", watchVerifyData.ElapsedMilliseconds);
                LogProvider.LogMessage(LogProvider.LogPriorities.Info, LogProvider.LogSources.CRYPTO_Basis, "GetHash: {0} ms", watchGetHash.ElapsedMilliseconds);
                LogProvider.LogMessage(LogProvider.LogPriorities.Info, LogProvider.LogSources.CRYPTO_Basis, "VerifyHash: {0} ms", watchVerifyHash.ElapsedMilliseconds);

                return returnValue;
            }
            catch (Exception error)
            {
                LogProvider.LogMessage(LogProvider.LogPriorities.Warning,
                    LogProvider.LogSources.CRYPTO_Basis,
                    "The following exception occurred while verifying data: {0}",
                    error);
                return false;
            }
        }
        
        /// <summary>
        /// Verify a signature from an explicit hash value.
        /// </summary>
        /// <param name="rgbHash"></param>
        /// <param name="halg"></param>
        /// <param name="signatureData"></param>
        /// <returns></returns>
        public override bool VerifyHash(byte[] rgbHash, object halg, byte[] signatureData)
        {
            /*
8.2.2 Signature verification operation

   RSASSA-PKCS1-V1_5-VERIFY ((n, e), M, S)

   Input:
   (n, e)   signer's RSA public key
   M        message whose signature is to be verified, an octet string
   S        signature to be verified, an octet string of length k, where
            k is the length in octets of the RSA modulus n

   Output:
   "valid signature" or "invalid signature"

   Errors: "message too long"; "RSA modulus too short"

   Steps:

   1. Length checking: If the length of the signature S is not k octets,
      output "invalid signature" and stop.

   2. RSA verification:

      a. Convert the signature S to an integer signature representative
         s (see Section 4.2):

            s = OS2IP (S).

Jonsson & Kaliski            Informational                     [Page 34]
--------------------------------------------------------------------------------

RFC 3447        PKCS #1: RSA Cryptography Specifications   February 2003


      b. Apply the RSAVP1 verification primitive (Section 5.2.2) to the
         RSA public key (n, e) and the signature representative s to
         produce an integer message representative m:

            m = RSAVP1 ((n, e), s).

         If RSAVP1 outputs "signature representative out of range,"
         output "invalid signature" and stop.

      c. Convert the message representative m to an encoded message EM
         of length k octets (see Section 4.1):

            EM' = I2OSP (m, k).

         If I2OSP outputs "integer too large," output "invalid
         signature" and stop.

   3. EMSA-PKCS1-v1_5 encoding: Apply the EMSA-PKCS1-v1_5 encoding
      operation (Section 9.2) to the message M to produce a second
      encoded message EM' of length k octets:

            EM' = EMSA-PKCS1-V1_5-ENCODE (M, k).

      If the encoding operation outputs "message too long," output
      "message too long" and stop.  If the encoding operation outputs
      "intended encoded message length too short," output "RSA modulus
      too short" and stop.

   4. Compare the encoded message EM and the second encoded message EM'.
      If they are the same, output "valid signature"; otherwise, output
      "invalid signature."

   Note.  Another way to implement the signature verification operation
   is to apply a "decoding" operation (not specified in this document)
   to the encoded message to recover the underlying hash value, and then
   to compare it to a newly computed hash value.  This has the advantage
   that it requires less intermediate storage (two hash values rather
   than two encoded messages), but the disadvantage that it requires
   additional code.

*/
            try
            {
                // Validate the parameters.
                if (rgbHash == null)
                {
                    throw new ArgumentNullException("rgbHash");
                }
                if (halg == null)
                {
                    throw new ArgumentNullException("halg");
                }
                if (signatureData == null)
                {
                    throw new ArgumentNullException("signatureData");
                }

                if (_rsaParams == null)
                {
                    throw new Exception(ErrorText.RSAParamsNotSet);
                }

                // Make sure that we have sufficient RSA parameters.
                if (_rsaParams.Value.Modulus == null)
                {
                    throw new Exception(ErrorText.RSAParamsNotSet);
                }

                int k = _rsaParams.Value.Modulus.Length;

                if (k < signatureData.Length)
                {
                    for (int i = 0; i < signatureData.Length - k; i++)
                    {
                        if (signatureData[i] != 0)
                        {
                            throw new CryptographicException("invalid signature");
                        }
                    }

                    byte[] old = signatureData;
                    signatureData = new byte[k];
                    Array.Copy(old, old.Length - k, signatureData, 0, k);
                }

                if (k != signatureData.Length)
                {
                    throw new CryptographicException("invalid signature");
                }

                string hashName = "";

                HashAlgorithm alg = ObjectToHashAlg(halg, out hashName);

                byte[] header = HashAlgToHeader(hashName);
                if (rgbHash.Length != header[header.Length - 1])
                {
                    throw new Exception(ErrorText.InvalidHashSize);
                }


                RsaCalculator rsaCalc = new RsaCalculator((RSAParameters)_rsaParams);

                string sigDataBinHex = ByteTools.ToBinHex(signatureData, "X");

                System.Diagnostics.Stopwatch watchDoCrypt = new System.Diagnostics.Stopwatch();
                watchDoCrypt.Start();
                string res = rsaCalc.DoCrypt(RsaCalculator.KeyType.RsaPublic, RsaCalculator.RsaDir.Decrypt, sigDataBinHex);
                watchDoCrypt.Stop();
                LogProvider.LogMessage(LogProvider.LogPriorities.Info, LogProvider.LogSources.CRYPTO_Basis, "VerifyHash.DoCrypt: {0} ms", watchDoCrypt.ElapsedMilliseconds);

                // Now extract the output
                byte[] EM1 = rsaCalc.BytesOutput;
                int l = EM1.Length;

                // the first 0 ist missing because of the implementation in RsaCalculator
                if (l + 1 != k)
                {
                    throw new CryptographicException("message too long");
                }

                byte[] EM2 = RsaPkcs1Padding(k, header, rgbHash);

                // compare
                // the first 0 ist missing because of the implementation in RsaCalculator
                for (int i = 1; i < k; i++)
                {
                    if (EM1[i - 1] != EM2[i])
                    {
                        return false;
                    }
                }

                return true;
            }
            catch (Exception error)
            {
                LogProvider.LogMessage(LogProvider.LogPriorities.Warning,
                    LogProvider.LogSources.CRYPTO_Basis,
                    "The following exception occurred while verifying data: {0}",
                    error);
                return false;
            }
        }
    }
}
